Voice leading in C major triads

The overdetermination of Western chords and scales leads ... composers ... [that are] interested in harmonic consistency, acoustic consonance, or scalar transposition ... necessarily ... toward the same familiar musical objects.(Dmitri Tymoczko, A Geometry of Music, chap 4, page 123)

Introduction

This notebook is a demostration of Orbichord, a project to explore the non-trivial topological quotient space of chords know as orbifolds, see references.

Tymoczko, Dmitri. "The geometry of musical chords." Science 313.5783 (2006): 72-74. Callender, Clifton, Ian Quinn, and Dmitri Tymoczko. "Generalized voice-leading spaces." Science 320.5874 (2008): 346-348. Dmitri Tymoczko, A Geometry of Music: Harmony and Counterpoint in the Extended Common Practice, Oxford University Press, 2011.

Orbichord comes from combining the words orbifold and chord. It is a collection of python modules I am writing build on top of music21 project.

https://web.mit.edu/music21/

Importing modules

Import music and graphite modules

In [1]:
# Import music modules
from music21.harmony import chordSymbolFigureFromChord
from music21.interval import Interval
from music21.scale import MajorScale
from music21.stream import Stream
from numpy import inf
from numpy import linalg as la
from orbichord.chordinate import EfficientVoiceLeading
from orbichord.graph import createGraph, convertGraphToData
from orbichord.generator import Generator
from orbichord.utils import renderWithLily, playAudio

import networkx as nx

# Import graphic modules
import pandas as pd
import holoviews as hv
from holoviews import opts, dim
from bokeh.sampledata.les_mis import data

hv.extension('bokeh')
hv.output(size=180)
defaults = dict(width=300, height=300, padding=0.1)
hv.opts.defaults(
    opts.EdgePaths(**defaults), opts.Graph(**defaults), opts.Nodes(**defaults))

Configuring a orbichord chord generator

Configure a chord generator using seven pitches of the C major scale. By default, chords are identified by the popular name with no inversion, resulting in the space chord defined by the set of pitch classes. We also force the chord to be triads, resulting in a generator for C major scale triad chords.

In [2]:
scale = MajorScale('C')

chord_generator = Generator(
    pitches=scale.getPitches('C','B'),
)

Define an efficient voice leading object using the C major scale to define voice leading steps. Define the metric max number of steps for all the voices $L_{\infty}$ norm.

In [3]:
max_norm_vl = EfficientVoiceLeading(
    scale = scale,
    metric = lambda delta: la.norm(delta, inf)
)

Create a chord graph passing as input a generator, voice leading objects, and tolerance function. The tolerance function provide the criteria to select how far chords can be connected by efficient voice leading.

In [4]:
graph, node_to_chord = createGraph(
    generator = chord_generator,
    voice_leading = max_norm_vl,
    tolerance = lambda x: x <= 1.0
)

Visualizing chord graph with holoview

Convert the chord networkx graph into a collection of links and nodes.

In [5]:
edges, vertices = convertGraphToData(graph)
links = pd.DataFrame(edges)
nodes = hv.Dataset(pd.DataFrame(vertices), 'index')
print(links.head())
nodes.data.head()
   source  target  value
0       0       1    1.0
1       0       2    1.0
2       0       3    1.0
3       0       4    1.0
4       0       5    1.0
Out[5]:
index name group
0 0 C 1
1 1 Am 1
2 2 F 1
3 3 Dm 1
4 4 Bdim 1

Visualize chord graph using (ironically) a chord graph.

In [7]:
chord = hv.Chord((links, nodes))
chord.opts(
    opts.Chord(
        cmap='Category20',
        edge_cmap='Category20',
        edge_color=dim('source').str(), 
        labels='name', node_color=dim('index').str()
    )
)
Out[7]:

Then the graph can be visualized directly

In [8]:
gview = hv.Graph.from_networkx(graph, nx.layout.circular_layout)
gview.opts(node_size=40)
labels = hv.Labels(gview.nodes, ['x', 'y'], 'index')
(gview * labels.opts(text_font_size='10pt', text_color='white', bgcolor='white'))
Out[8]:

Conclusion

All the triad chords are at one step away from each other in the space of set of pitch classes. This is the case when allowing voice leading to change multiple voices. However, each voice can move at the most one step of the C major scale. However, looking at the C major triad or the neighbor chords of C major, we found for example that G major seems to be transpose 5 step from C major.

In [5]:
stream = Stream()
interval4 = Interval(4*12)

node = 'C'
neighbors = [node]
for neighbor, edge in graph.adj[node].items():
    neighbors.append(neighbor)
neighbors.sort(key = lambda name: name[0])
size = len(neighbors)
neighbors = [neighbors[(i+2)%size] for i in range(size)]

for neighbor in neighbors:
    chord = node_to_chord[neighbor].transpose(interval4)
    chord.addLyric(chordSymbolFigureFromChord(chord))
    chord.duration.type = 'whole'
    stream.append(chord)

renderWithLily(stream)
Out[5]:
In [6]:
playAudio(stream)
Out[6]:

But then, in what sense C and G major chords are only one step of the major scale?

In [ ]: